using System;
using System.Collections.Generic;
using System.Text;
using System.Net;
using System.Net.Sockets;
using System.Threading;

namespace LumiSoft.Net.TCP
{    
    /// <summary>
    /// This class implements generic TCP session based server.
    /// </summary>
    public class TCP_Server<T> : IDisposable where T : TCP_ServerSession,new()
    {
        #region delegate TCP_ServerSessionHandler

        /// <summary>
        /// This method provides data to <b>SessionCreated</b> event.
        /// </summary>
        /// <param name="e"></param>
        public delegate void TCP_ServerSessionHandler(TCP_ServerSessionEventArgs<T> e);

        #endregion

        #region class ListeningPoint

        /// <summary>
        /// This class holds listening point info.
        /// </summary>
        private class ListeningPoint
        {
            private Socket     m_pSocket   = null;
            private IPBindInfo m_pBindInfo = null;

            /// <summary>
            /// Default constructor.
            /// </summary>
            /// <param name="socket">Listening socket.</param>
            /// <param name="bind">Bind info what acceped socket.</param>
            public ListeningPoint(Socket socket,IPBindInfo bind)
            {
                m_pSocket   = socket;
                m_pBindInfo = bind;
            }


            #region Properties Implementation

            /// <summary>
            /// Gets socket.
            /// </summary>
            public Socket Socket
            {
                get{ return m_pSocket; }
            }

            /// <summary>
            /// Gets bind info.
            /// </summary>
            public IPBindInfo BindInfo
            {
                get{ return m_pBindInfo; }
            }

            #endregion

        }

        #endregion

        private bool                                     m_IsDisposed           = false;
        private bool                                     m_IsRunning            = false;
        private IPBindInfo[]                             m_pBindings            = new IPBindInfo[0];
        private bool                                     m_HasBindingsChanged   = false;
        private long                                     m_MaxConnections       = 0;
        private long                                     m_MaxConnectionsPerIP  = 0;  // TODO:
        private int                                      m_SessionIdleTimeout   = 100;
        private DateTime                                 m_StartTime;
        private long                                     m_ConnectionsProcessed = 0;
        private TCP_SessionCollection<TCP_ServerSession> m_pSessions            = null;

        /// <summary>
        /// Default constructor.
        /// </summary>
        public TCP_Server()
        {
            m_pSessions = new TCP_SessionCollection<TCP_ServerSession>();
        }

        #region method Dispose

        /// <summary>
        /// Cleans up any resources being used.
        /// </summary>
        public void Dispose()
        {
            if(m_IsDisposed){
                return;
            }
            m_IsDisposed = true;
            m_IsRunning  = false;

            // We must call disposed event before we release events.
            try{
                OnDisposed();
            }
            catch{
                // We never should get exception here, user should handle it, just skip it.
            }

            m_pSessions = null;

            // Release all events.
            this.Started  = null;
            this.Stopped  = null;
            this.Disposed = null;
            this.Error    = null;
        }

        #endregion


        #region method Start

        /// <summary>
        /// Starts TCP server.
        /// </summary>
        /// <exception cref="ObjectDisposedException">Is raised when this object is disposed and this property is accessed.</exception>
        public void Start()
        {
            if(m_IsDisposed){
                throw new ObjectDisposedException("TCP_Server");
            }
            if(m_IsRunning){
                return;
            }
            m_IsRunning = true;

            m_StartTime = DateTime.Now;
            m_ConnectionsProcessed = 0;

            Thread tr = new Thread(new ThreadStart(this.Run));
            tr.Start();

            OnStarted();
        }

        #endregion

        #region method Stop

        /// <summary>
        /// Stops TCP server, all active connections will be terminated.
        /// </summary>
        public void Stop()
        {
            if(!m_IsRunning){
                return;
            }
            m_IsRunning = false;

            OnStopped();
        }

        #endregion

        #region method Restart

        /// <summary>
        /// Restarts TCP server.
        /// </summary>
        public void Restart()
        {
            Stop();
            Start();
        }

        #endregion


        #region method Run

        /// <summary>
        /// TCP server run loop.
        /// </summary>
        private void Run()
        {
            // We just accept connections and queue their processing on thread pool thread.
            CircleCollection<ListeningPoint> listeningPoints = new CircleCollection<ListeningPoint>();
            while(m_IsRunning){
                try{
                    // Start listen, monitor bindings change.
                    if(m_HasBindingsChanged){
                        // Dispose all old binds.
                        foreach(ListeningPoint listeningPoint in listeningPoints.ToArray()){
                            try{
                                listeningPoint.Socket.Close();
                            }
                            catch(Exception x){
                                OnError(x);
                            }
                        }
                        listeningPoints.Clear();

                        // Create new listening points.
                        foreach(IPBindInfo bind in m_pBindings){
                            try{
                                Socket socket = null;
                                if(bind.IP.AddressFamily == AddressFamily.InterNetwork){
                                    socket = new Socket(AddressFamily.InterNetwork,SocketType.Stream,ProtocolType.Tcp);
                                }
                                else if(bind.IP.AddressFamily == AddressFamily.InterNetworkV6){
                                    socket = new Socket(AddressFamily.InterNetworkV6,SocketType.Stream,ProtocolType.Tcp);
                                }
                                else{
                                    // Invalid address family, just skip it.
                                    continue;
                                }
                                listeningPoints.Add(new ListeningPoint(socket,bind));                            
                            }
                            catch(Exception x){
                                // The only exception what we should get there is if socket is in use.
                                OnError(x);
                            }
                        }
                    }

                    // Maximum connections exceeded, just sleep a little and try again with next while loop.
                    if(m_pSessions.Count > m_MaxConnections){
                        Thread.Sleep(10);
                    }
                    // We have listening points, see if next listening point has incoming connection.
                    else if(listeningPoints.Count > 0){
                        ListeningPoint listeningPoint = listeningPoints.Next();
                        if(listeningPoint.Socket.Poll(0,SelectMode.SelectRead)){
                            // Accept connection.
                            Socket connectedSocket = listeningPoint.Socket.Accept();

                            // Queue connection processing to thread pool thread.
                            ThreadPool.QueueUserWorkItem(new WaitCallback(this.ProcessQueuedConnection),new ListeningPoint(connectedSocket,listeningPoint.BindInfo));
                            m_ConnectionsProcessed++;
                        }
                        else{
                            Thread.Sleep(1);
                        }
                    }
                    // We have no listening points, so just skip.
                    else{
                        Thread.Sleep(200);
                    }                    
                }
                catch(Exception x){
                    OnError(x);
                }
            }
        }

        #endregion

        #region method ProcessQueuedConnection

        /// <summary>
        /// Processes queued TCP server connection.
        /// </summary>
        /// <param name="state">State data.</param>
        private void ProcessQueuedConnection(object state)
        {
            try{
                ListeningPoint connection = (ListeningPoint)state;

                T session = new T();
                session.Init(connection.Socket,connection.BindInfo.SslMode == SslMode.SSL,connection.BindInfo.Certificate);
                m_pSessions.Add(session);
                session.Start();

                OnSessionCreated(session);
            }
            catch(Exception x){
                OnError(x);
            }
        }

        #endregion

                        
        #region Properties Implementation

        /// <summary>
        /// Gets if server is disposed.
        /// </summary>
        public bool IsDisposed
        {
            get{ return m_IsDisposed; }
        }

        /// <summary>
        /// Gets if server is running.
        /// </summary>
        public bool IsRunning
        {
            get{ return m_IsRunning; }
        }
                
        /// <summary>
        /// Gets or sets TCP server IP bindings.
        /// </summary>
        /// <exception cref="ObjectDisposedException">Is raised when this object is disposed and this property is accessed.</exception>
        public IPBindInfo[] Bindings
        {
            get{
                if(m_IsDisposed){
                    throw new ObjectDisposedException("TCP_Server");
                }

                return m_pBindings; 
            }

            set{
                if(m_IsDisposed){
                    throw new ObjectDisposedException("TCP_Server");
                }
                if(value == null){
                    value = new IPBindInfo[0];
                }

                //--- See binds has changed --------------
                bool changed = false;
                if(m_pBindings.Length != value.Length){
                    changed = true;
                }
                else{
                    for(int i=0;i<m_pBindings.Length;i++){
                        if(!m_pBindings[i].Equals(value[i])){
                            changed = true;
                            break;
                        }
                    }
                }

                if(changed){
                    m_pBindings = value;
                    m_HasBindingsChanged = true;
                }
            }
        }

        /// <summary>
        /// Gets or sets maximum allowed concurent connections. Value 0 means unlimited.
        /// </summary>
        /// <exception cref="ObjectDisposedException">Is raised when this object is disposed and this property is accessed.</exception>
        /// <exception cref="ArgumentException">Is raised when negative value is passed.</exception>
        public long MaxConnections
        {
            get{
                if(m_IsDisposed){
                    throw new ObjectDisposedException("TCP_Server");
                }
                
                return m_MaxConnections; 
            }

            set{
                if(m_IsDisposed){
                    throw new ObjectDisposedException("TCP_Server");
                }
                if(value < 0){
                    throw new ArgumentException("Property 'MaxConnections' value must be >= 0.");
                }

                m_MaxConnections = value;
            }
        }

        /// <summary>
        /// Gets or sets maximum allowed connections for 1 IP address. Value 0 means unlimited.
        /// </summary>
        public long MaxConnectionsPerIP
        {
            get{ 
                if(m_IsDisposed){
                    throw new ObjectDisposedException("TCP_Server");
                }
                
                return m_MaxConnectionsPerIP; 
            }

            set{
                if(m_IsDisposed){
                    throw new ObjectDisposedException("TCP_Server");
                }
                if(m_MaxConnectionsPerIP < 0){
                    throw new ArgumentException("Property 'MaxConnectionsPerIP' value must be >= 0.");
                }

                m_MaxConnectionsPerIP = value;
            }
        }

        /// <summary>
        /// Gets or sets maximum allowed session idle time in seconds, after what session will be terminated. Value 0 means unlimited,
        /// but this is strongly not recommened.
        /// </summary>
        /// <exception cref="ObjectDisposedException">Is raised when this object is disposed and this property is accessed.</exception>
        /// <exception cref="ArgumentException">Is raised when negative value is passed.</exception>
        public int SessionIdleTimeout
        {
            get{ 
                if(m_IsDisposed){
                    throw new ObjectDisposedException("TCP_Server");
                }
                
                return m_SessionIdleTimeout; 
            }

            set{
                if(m_IsDisposed){
                    throw new ObjectDisposedException("TCP_Server");
                }
                if(m_SessionIdleTimeout < 0){
                    throw new ArgumentException("Property 'SessionIdleTimeout' value must be >= 0.");
                }

                m_SessionIdleTimeout = value;
            }
        }

        /// <summary>
        /// Gets the time when server was started.
        /// </summary>
        /// <exception cref="ObjectDisposedException">Is raised when this object is disposed and this property is accessed.</exception>
        /// <exception cref="InvalidOperationException">Is raised when TCP server is not running and this property is accesed.</exception>
        public DateTime StartTime
        {
            get{
                if(m_IsDisposed){
                    throw new ObjectDisposedException("TCP_Server");
                }
                if(!m_IsRunning){
                    throw new InvalidOperationException("TCP server is not running.");
                }

                return m_StartTime; 
            }
        }
                
        /// <summary>
        /// Gets how many connections this TCP server has processed.
        /// </summary>
        /// <exception cref="ObjectDisposedException">Is raised when this object is disposed and this property is accessed.</exception>
        /// <exception cref="InvalidOperationException">Is raised when TCP server is not running and this property is accesed.</exception>
        public long ConnectionsProcessed
        {
            get{ 
                if(m_IsDisposed){
                    throw new ObjectDisposedException("TCP_Server");
                }
                if(!m_IsRunning){
                    throw new InvalidOperationException("TCP server is not running.");
                }

                return m_ConnectionsProcessed; 
            }
        }

        /// <summary>
        /// Gets TCP server active sessions.
        /// </summary>
        /// <exception cref="ObjectDisposedException">Is raised when this object is disposed and this property is accessed.</exception>
        /// <exception cref="InvalidOperationException">Is raised when TCP server is not running and this property is accesed.</exception>
        public TCP_SessionCollection<TCP_ServerSession> Sessions
        {
            get{ 
                if(m_IsDisposed){
                    throw new ObjectDisposedException("TCP_Server");
                }
                if(!m_IsRunning){
                    throw new InvalidOperationException("TCP server is not running.");
                }

                return m_pSessions; 
            }
        }


        #endregion

        #region Events Implementation

        /// <summary>
        /// This event is raised when TCP server has started.
        /// </summary>
        public event EventHandler Started = null;

        #region method OnStarted

        /// <summary>
        /// Raises <b>Started</b> event.
        /// </summary>
        protected void OnStarted()
        {
            if(this.Started != null){
                this.Started(this,new EventArgs());
            }
        }

        #endregion

        /// <summary>
        /// This event is raised when TCP server has stopped.
        /// </summary>
        public event EventHandler Stopped = null;

        #region method OnStopped

        /// <summary>
        /// Raises <b>Stopped</b> event.
        /// </summary>
        protected void OnStopped()
        {
            if(this.Stopped != null){
                this.Stopped(this,new EventArgs());
            }
        }

        #endregion

        /// <summary>
        /// This event is raised when TCP server has disposed.
        /// </summary>
        public event EventHandler Disposed = null;

        #region method OnDisposed

        /// <summary>
        /// Raises <b>Disposed</b> event.
        /// </summary>
        protected void OnDisposed()
        {
            if(this.Disposed != null){
                this.Disposed(this,new EventArgs());
            }
        }

        #endregion

        /// <summary>
        /// This event is raised when TCP server creates new session.
        /// </summary>
        public event TCP_ServerSessionHandler SessionCreated = null;

        #region method OnSessionCreated

        /// <summary>
        /// Raises <b>SessionCreated</b> event.
        /// </summary>
        /// <param name="session">TCP server session that was created.</param>
        private void OnSessionCreated(T session)
        {
            if(this.SessionCreated != null){
                this.SessionCreated(new TCP_ServerSessionEventArgs<T>(this,session));
            }
        }

        #endregion
                        
        /// <summary>
        /// This event is raised when TCP server has unknown unhandled error.
        /// </summary>
        public event ErrorEventHandler Error = null;

        #region method OnError

        /// <summary>
        /// Raises <b>Error</b> event.
        /// </summary>
        /// <param name="x">Exception happened.</param>
        private void OnError(Exception x)
        {
            if(this.Error != null){
                this.Error(this,new Error_EventArgs(x,new System.Diagnostics.StackTrace()));
            }
        }

        #endregion

        #endregion

    }
}
